/**
 * File name: DenpendencyFinderXMLReader.java Author: Willard Wang Create date:
 * Jun 19, 2013
 */
package edu.sjtu.se.prophet.io;

import java.io.BufferedReader;
import java.io.File;
// import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;
// import org.apache.commons.digester3.Digester;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xml.sax.SAXException;

import edu.sjtu.se.prophet.exception.EdgeExistsException;
import edu.sjtu.se.prophet.exception.InvalidDataSourceException;
import edu.sjtu.se.prophet.exception.NodeExistsException;
import edu.sjtu.se.prophet.exception.NodeNotFoundException;
import edu.sjtu.se.prophet.graph.Edge;
import edu.sjtu.se.prophet.graph.Graph;
import edu.sjtu.se.prophet.graph.Node;
// import edu.sjtu.se.prophet.io.xml.Dependencies;
// import edu.sjtu.se.prophet.io.xml.Feature;
// import edu.sjtu.se.prophet.io.xml.InBound;
// import edu.sjtu.se.prophet.io.xml.OutBound;

/**
 * @author deathspeeder
 * 
 */
public class DenpendencyFinderXMLReader extends IOCommon implements GraphReader {
    private static Logger logger = Logger
            .getLogger(DenpendencyFinderXMLReader.class);

    static class ReadGraphThread extends Thread {
        String fileName;
        String version;
        Graph graph;

        public ReadGraphThread(String fileName, String version, Graph graph) {
            this.fileName = fileName;
            this.version = version;
            this.graph = graph;
        }

        @Override
        public void run() {
            try {
                readGraph(fileName, version, graph);
            } catch (Exception e) {
                logger.error("Read graph error", e);
            }
        }
    }

    private String revisionsFile = "revisions";

    private String functionsFile = "r2m.txt"; // TODO count bug number on
                                              // revision

    public DenpendencyFinderXMLReader() {
    }

    /*
     * (non-Javadoc)
     * 
     * @see edu.sjtu.se.prophet.io.SettableIO#setValue(java.lang.String,
     * java.lang.String)
     */
    @Override
    public void setValue(String key, String value) {
        if ("revisionsFile".equals(key))
            revisionsFile = value;
        else if ("functionsFile".equals(key))
            functionsFile = value;
        else if ("files".equals(key)) {
            for (String str : value.split("[,;:]")) {
                addSource(str);
            }
        }

    }

    /*
     * (non-Javadoc)
     * 
     * @see edu.sjtu.se.prophet.io.GraphReader#addSource(java.lang.String)
     */
    @Override
    public void addSource(String uri) {
        if (uri.endsWith(functionsFile))
            functionsFile = uri;
        if (uri.endsWith(revisionsFile))
            revisionsFile = uri;
        if (uri.endsWith("xml"))
            uris.add(uri);
    }

    /*
     * (non-Javadoc)
     * 
     * @see edu.sjtu.se.prophet.io.GraphReader#read()
     */
    @Override
    public List<Graph> read() throws IOException, InvalidDataSourceException {
        readRevisions();
        try {
            readGraphs();
        } catch (DocumentException e) {
            throw new InvalidDataSourceException(e.getMessage(), e);
        }
        readFunctions();
        // readEdges();
        //
        // long totalFunctions = 0;
        // long totalEdges = 0;
        // long totalBuggyFunctions = 0;
        // long totalBuggyEdges = 0;
        //
        // for (int i = 0; i < graphs.size(); i++) {
        // System.out
        // .println("Debug: " + graphs.get(i).getMeta().get(VERSION));
        // for (Iterator<Edge> it = graphs.get(i).getEdges().iterator(); it
        // .hasNext();) {
        // Edge edge = it.next();
        // totalEdges++;
        // if ((Integer) edge.getMetrics().get(BUG_COUNT) > 0) {
        // System.out.println(edge);
        // totalBuggyEdges++;
        // }
        // }
        // for (Iterator<Node> it = graphs.get(i).getNodes().iterator(); it
        // .hasNext();) {
        // Node node = it.next();
        // totalFunctions++;
        // if ((Integer) node.getMetrics().get(BUG_COUNT) > 0) {
        // System.out.println(node);
        // totalBuggyFunctions++;
        // }
        // }
        // }
        // System.out.println("Debug: total nodes " + totalFunctions);
        // System.out.println("Debug: total buggy nodes " +
        // totalBuggyFunctions);
        // System.out.println("Debug: node buggy ratio "
        // + (double) totalBuggyFunctions / totalFunctions);
        // System.out.println("Debug: total edges " + totalEdges);
        // System.out.println("Debug: total buggy edges " + totalBuggyEdges);
        // System.out.println("Debug: edge buggy ratio "
        // + (double) totalBuggyEdges / totalEdges);
        return graphs;
    }

    /**
     * @throws IOException
     * 
     */
    private void readEdges() throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(functionsFile));
        Map<Integer, Set<String>> revToFunc = new HashMap<Integer, Set<String>>();
        String line = null;
        while ((line = br.readLine()) != null) {
            logger.info("Read line " + line);
            if ("".equals(line.trim()))
                continue;
            String[] tokens = line.trim().split("\\t|\\s");
            if (tokens.length > 1) {
                int rev = Integer.parseInt(tokens[0]);
                if (!revToFunc.containsKey(rev))
                    revToFunc.put(rev, new HashSet<String>());
                for (int i = 1; i < tokens.length; i++) {
                    revToFunc.get(rev).add(tokens[i]);
                }
            } else {
                logger.info("Ignored line: " + line);
            }
        }
        br.close();

        for (Graph g : graphs) {
            for (Edge e : g.getEdges()) {
                e.getMetrics().put(BUG_COUNT, 0);
            }
        }

        for (Integer rev : revToFunc.keySet()) {
            String version = getVersionOfRevision(rev);
            Graph graph = getGraphOfVersion(version);
            if (graph != null) {
                Set<String> funcs = revToFunc.get(rev);
                Map<String, Node> funcToNode = new HashMap<String, Node>();
                for (String func : funcs) {
                    for (Iterator<Node> it = graph.getNodes().iterator(); it
                            .hasNext();) {
                        Node node = it.next();
                        if (fuzzyMatch(node.getId(), func)) {
                            funcToNode.put(func, node);
                        }
                    }
                }

                for (String start : funcToNode.keySet()) {
                    for (String end : funcToNode.keySet()) {
                        Edge edge = graph.findEdge(funcToNode.get(start)
                                .getId(), funcToNode.get(end).getId());
                        if (edge != null) {
                            int count = (Integer) edge.getMetrics().get(
                                    BUG_COUNT);
                            edge.getMetrics().put(BUG_COUNT, count + 1);
                        }
                    }
                }
            }
        }
    }

    /**
     * @throws IOException
     * 
     */
    private void readFunctions() throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(functionsFile));
        String line = null;
        while ((line = br.readLine()) != null) {
            logger.info("Read line " + line);
            if ("".equals(line.trim()))
                continue;
            String[] tokens = line.trim().split("\\t|\\s");
            if (tokens.length > 1) {
                int rev = Integer.parseInt(tokens[0]);
                String version = getVersionOfRevision(rev);
                Graph graph = getGraphOfVersion(version);
                if (graph == null) {
                    logger.error("Graph id null for version: " + version);
                    System.exit(-1);
                }
                for (int i = 1; i < tokens.length; i++) {
                    addBug(graph, tokens[i]);
                }
            } else {
                logger.info("Ignored line: " + line);
            }
        }
        br.close();

    }

    /**
     * @param graph
     * @param string
     */
    private void addBug(Graph graph, String method) {
        boolean match = false;
        for (Iterator<Node> it = graph.getNodes().iterator(); it.hasNext();) {
            Node node = it.next();
            if (!node.getMetrics().containsKey(BUG_COUNT))
                node.getMetrics().put(BUG_COUNT, 0);
            if (fuzzyMatch(node.getId(), method)) {
                Integer count = (Integer) node.getMetrics().get(BUG_COUNT);
                node.getMetrics().put(BUG_COUNT, count + 1);
                match = true;
                logger.info("Matched method: " + method + " on " + node.getId());
                break;
            }
        }

        if (!match)
            logger.info("Ignored method: " + method + " on version "
                    + graph.getMeta().get(VERSION));

    }

    /**
     * @param rev
     * @return
     */
    private String getVersionOfRevision(int rev) {
        for (int i = revisions.size() - 1; i > 0; i--) {
            if (revisions.get(i) <= rev)
                return versions.get(i);
        }
        return versions.get(0);
    }

    /**
     * @throws SAXException
     * @throws IOException
     * @throws DocumentException
     * 
     */
    private void readGraphs() throws IOException, DocumentException {
        if (uris.size() != versions.size()) {
            logger.error("Number of versions (" + versions.size()
                    + ") does not match with number of XML files ("
                    + uris.size() + ").");
            System.exit(-1);
        }

        List<Thread> threads = new ArrayList<Thread>();
        for (int i = 0; i < uris.size(); i++) {
            Graph graph = new Graph();
            graphs.add(graph);
            Thread t = new ReadGraphThread(uris.get(i), versions.get(i), graph);
            threads.add(t);
            t.start();
        }

        try {
            for (Thread t : threads) {
                t.join();
            }
        } catch (InterruptedException e) {
            logger.error("Unexpected", e);
        }
    }

    private static void addNode(Graph graph, String id) {
        try {
            if (id.endsWith(")"))
                graph.addNode(id);
        } catch (NodeExistsException e) {
            // ignore
            // logger.warn("When adding node " + id, e);
        }
    }

    private static void addEdge(Graph graph, String from, String to) {
        try {
            if (from.endsWith(")") && to.endsWith(")"))
                graph.addEdge(from, to);
        } catch (NodeNotFoundException e) {
            // ignore
            // logger.warn("When adding edge from " + from + " to " + to, e);
        } catch (EdgeExistsException e) {
            // ignore
            // logger.warn("When adding edge from " + from + " to " + to, e);
        }
    }

    /**
     * @param string
     * @return
     * @throws SAXException
     * @throws IOException
     * @throws DocumentException
     */
    private static void readGraph(String fileName, String version, Graph graph)
            throws IOException, DocumentException {

        Document doc = new SAXReader().read(new File(fileName));
        // Graph graph = new Graph();
        graph.getMeta().put(VERSION, version);
        Element root = doc.getRootElement();
        logger.info("Reading graph " + version + " ...");
        for (Iterator<Element> p = root.elementIterator("package"); p.hasNext();) {
            Element packageElement = p.next();
            if ("yes".equals(packageElement.attribute("confirmed").getText())) {
                for (Iterator<Element> c = packageElement
                        .elementIterator("class"); c.hasNext();) {
                    Element classElement = c.next();
                    if ("yes".equals(classElement.attribute("confirmed")
                            .getText())) {
                        for (Iterator<Element> f = classElement
                                .elementIterator("feature"); f.hasNext();) {
                            Element featureElement = f.next();
                            if ("yes".equals(featureElement.attribute(
                                    "confirmed").getText())) {
                                addNode(graph, featureElement.element("name")
                                        .getText());

                                for (Iterator<Element> i = featureElement
                                        .elementIterator("inbound"); i
                                        .hasNext();) {
                                    Element inElement = i.next();
                                    if ("yes".equals(inElement
                                            .attributeValue("confirmed"))
                                            && "feature".equals(inElement
                                                    .attributeValue("type"))) {
                                        addNode(graph, inElement.getText());
                                        addEdge(graph, inElement.getText(),
                                                featureElement.element("name")
                                                        .getText());
                                    }
                                }

                                for (Iterator<Element> o = featureElement
                                        .elementIterator("outbound"); o
                                        .hasNext();) {
                                    Element outElement = o.next();
                                    if ("yes".equals(outElement
                                            .attributeValue("confirmed"))
                                            && "feature".equals(outElement
                                                    .attributeValue("type"))) {
                                        addNode(graph, outElement.getText());
                                        addEdge(graph,
                                                featureElement.element("name")
                                                        .getText(),
                                                outElement.getText());
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        for (Node node : graph.getNodes()) {
            node.getMetrics().put(BUG_COUNT, 0);
        }
        // return graph;
    }

    private final List<String> versions = new ArrayList<String>();
    private final List<Integer> revisions = new ArrayList<Integer>();

    /**
     * @throws IOException
     * 
     */
    private void readRevisions() throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(revisionsFile));
        String line = null;
        while ((line = br.readLine()) != null) {
            logger.info("Read line " + line);
            if ("".equals(line.trim()))
                continue;
            String[] tokens = line.trim().split("\\t|\\s");
            if (tokens.length > 1) {
                versions.add(tokens[0]);
                revisions.add(Integer.parseInt(tokens[1]));
                logger.info("Added ver and rev: " + tokens[0] + ", "
                        + tokens[1]);
            }
        }
        br.close();
    }

}
